module Navegador
  class Generator
    attr_reader :filters, :actions, :search_fields, :buttons
    
    def initialize
      @filters = []
      @actions = []
      @search_fields = []
      @buttons = []
    end
    
    def filter(*args, &block)
      options = args.last.is_a?(Hash) ? args.pop : {}
      method = args.shift
      conditions = !args.empty? ? args.shift : options[:conditions]
      label = options[:label] || method.to_s.humanize
      label = 'Mostrar todo' if method == :all && !options[:label]
      @filters << [method, label, conditions_to_s(conditions), block]
    end
    
    def filter_methods
      @filters.map(&:first)
    end
    
    def filters(name = nil)
      name && !name.to_s.empty? ? @filters.find {|f| f.first == name.to_sym} : @filters
    end
    
    def action(name, options = {}, &block)
      label = options[:label] || name.to_s.humanize
      @actions << [name, label, options, block]
    end
    
    def search(*fields)
      @search_fields = fields
    end
    
    def button(options)
      @buttons << options
    end
    
    private
    
    # El valor de conditions puede ser un array, string o hash
    def conditions_to_s(conditions)
      s = nil
      if conditions.is_a?(Array)
        conditions = conditions.map {|c| condition_param_to_s(c)}
        s = '[' + conditions.join(',') + ']'
      elsif conditions.is_a?(String)
        s = "\"#{conditions}\""
      elsif conditions.is_a?(Hash)
        raise "Actualmente no se soporta especificar un hash para el valor de la clave :conditions"
        #TODO: Generar cuando es un Hash...
      end
      s
    end   
    
    def condition_param_to_s(param)
      if param.is_a?(String)
        "\"#{param}\""
      elsif param.is_a?(Symbol)
        ":#{param}"
      elsif param.is_a?(TrueClass) || param.is_a?(FalseClass)
        param ? 'true' : 'false'
      else
        param
      end
    end
  end
  
  class CodeGenerator
    def initialize(generator, modelo, opciones = {})
      @generator = generator
      @modelo = modelo.to_s
      @modelos = @modelo.pluralize
      @modelo_class = @modelo.classify
      @controller_sym = ":#{@modelo.to_s.pluralize}_controller"
      @controller_class = (@modelo.to_s.pluralize + '_controller').classify
      @opciones = opciones
    end
    
    def index_action
      <<-end_src
        get_list
        @#{@modelo} = #{@modelo_class}.new
        before_render
        render_index
      end_src
    end
    
    def render_index_protected
      <<-end_src
        respond_to do |format|
          format.html do
            erb = <<-end_erb
              <div id="#{model_name}_grid">
                <%= render :partial => 'list' %>
              </div>
            end_erb
            render :inline => erb, :layout => true
          end
          format.js do
            render :update do |page|
              page['#{@modelo}_grid'].replace :partial => '#{@modelos}/list'
            end
          end
        end
      end_src
    end
    
    def new_action
      <<-end_src
        @current = @#{@modelo} = #{@modelo_class}.new
        before_render
        respond_to do |format|
          format.html do
            render :layout => true, :inline => <<-inline_src
              <% new_form_for do |f| %>
                <%= render :partial => 'form', :object => f %>
                <%= f.actions :delete => false %>
              <% end %>
            inline_src
          end
          format.js do 
            locals = get_inline_form_locals
            render :update do |page|
              page.inline_blind_down("#{@modelo}", locals)
            end
          end
        end
      end_src
    end
    
    def show_action
      <<-end_src
        before_render
        respond_to do |format|
          format.html
          format.xml  { render :xml => @#{@modelo}.to_xml }
          format.js   { render 'show.rjs' }
        end
      end_src
    end
    
    def create_action
      code = <<-end_src
        before_assign
        @current = @#{@modelo} = #{@modelo_class}.new(params[:#{@modelo}])
      end_src
      if parent = @opciones[:parent]
        parent_param = ", :#{parent}_id => @parent"
        code += <<-end_src
          @#{@modelo}.#{parent_key} = @parent.id
        end_src
      end
      code += <<-end_src
        save_model
        @key = @success ? :success : :error
        @msg = @success ? "Registro creado." : "Error al crear el Registro."
        before_render
        respond_to do |format|
          format.html do
            flash[@key] = @msg
            if @success
              redirect_to :action => 'index' #{parent_param}
            else
              render '#{@modelos}/new'
            end
          end
          format.js {update_record_ajax}
        end
      end_src
      code
    end
    
    def edit_action
      <<-end_src
        before_render
        respond_to do |format|
          format.html {
            render :layout => true, :inline => <<-inline_src
              <% edit_form_for do |f| %>
                <%= render :partial => 'form', :object => f %>
                <%= f.actions %>
              <% end %>
            inline_src
          }
          format.js do
            locals = get_inline_form_locals
            render :update do |page|
              page.inline_blind_down("#{@modelo}", locals)
            end
          end
        end
      end_src
    end
    
    def update_action
      if parent = @opciones[:parent]
        parent_param = ", :#{parent}_id => @parent"
      end
      <<-end_src
        before_assign
        @#{@modelo}.attributes = params[:#{@modelo}]
        save_model
        @key = @success ? :success : :error
        @msg = @success ? 'Datos actualizados.' : 'Error al actualizar los datos.'
        respond_to do |format|
          format.html do
            flash[@key] = @msg
            if @success
              redirect_to :action => 'show', :id => @#{@modelo} #{parent_param}
            else
              render '#{@modelos}/edit'
            end
          end
          format.js {update_record_ajax}
        end
        before_render
      end_src
    end
    
    def destroy_action
      <<-end_src
        @success = @#{@modelo}.destroy
        @key = @success ? :success : :error
        @msg = @success ? "Registro eliminado." : "Error al eliminar el registro."
        respond_to do |format|
          format.html do
            flash[@key] = @msg
            if @success
              redirect_to #{@modelos}_url
            else
              redirect_to edit_#{@modelo}_url
            end
          end
          format.js {update_record_ajax}
        end
        before_render
      end_src
    end
    
    def save_model_protected
      <<-end_src
        begin
          #{@modelo.classify}.transaction do 
            before_save
            if @success = @#{@modelo}.save!
              after_save
            end
          end 
        rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid
          logger.error("Ocurrió una exception al salvar el registro: " + $!)
          @success = false
        end
      end_src
    end
    
    def get_inline_form_locals_protected
      code = <<-end_src
        {:model => :#{@modelo}, :current => @#{@modelo},
         :parent => #{@controller_class}.options[:parent]}
      end_src
    end
    
    def update_record_ajax_protected
      <<-end_src
        get_list
        locals = get_inline_form_locals
        render :update do |page|
          inline = '#{@modelo}_inline_form'
          if @success
            page.hide_inline(inline, show_flash(@key => @msg), @#{@modelo})
          else
            page.update_inline(inline, locals)
          end
          page.hide_flash(@key)
        end
      end_src
    end
    
    def get_list_protected
      if @generator.filters.empty?
        code = <<-end_src
          get_list_find
        end_src
      else
        all_filter = @generator.filters(:all)
        all_conditions = all_filter && all_filter[2] ? all_filter[2] : 'nil'
        code = <<-end_src
          #{@modelo_class}.with_scope(:find => {:conditions => #{all_conditions}}) do
            #{work_with_filters}
          end
        end_src
      end
    end
    
    def work_with_filters
      code = <<-end_src
        filter = params[:filter] || session[:#{@modelo}_filter]
        session[:#{@modelo}_filter] = filter
        case filter
      end_src
      @generator.filters.each do |f|
        method, label, conditions, block = f
        str_conditions = conditions || "nil"
        code += <<-end_src
          when '#{method}'
            #{@modelo_class}.with_scope(:find => {:conditions => #{str_conditions}}) do
              get_list_find
            end
        end_src
      end      
      code += <<-end_src
          else
            get_list_find
        end  #end when#
      end_src
    end
    
    def get_list_find_protected
      code = <<-end_src
        if params[:search]
          fields = #{@controller_class}.search_fields
          fields = fields.map {|field| field.to_s + " LIKE '%" + params[:search] + "%'"}
          conditions = '(' + fields.join(' OR ') + ')'
        else
          conditions = ''
        end
      end_src
      if parent = @opciones[:parent]
        code += <<-end_src
          conditions += '#{parent_key} = ' + @parent.id.to_s
        end_src
      end
      code += <<-end_src
        conditions = nil if conditions.empty?
        @#{@modelos} = #{@modelo_class}.find(:all, :conditions => conditions, 
                                                   :order => get_order)
      end_src
      apply_filters(code)
    end
    
    def apply_filters(code)
      <<-end_src
        block = Proc.new do
          #{code}
        end
        # Permitir utilizar with_scope en el metodo list_scope() declarado en el controller.
        methods.include?('list_scope') ? list_scope(&block) : block.call
        @current_filter = 'all'
        apply_filter
        @current_filter = session[:#{@modelo}_filter].to_s
        apply_filter if @current_filter != 'all'
        create_paginator
      end_src
    end
    
    # Aplicar la selección del filtro indicado
    def apply_filter_protected
      <<-end_src
        filter = self.class.filters(@current_filter)
        if filter && filter.last.is_a?(Proc)
          @#{@modelos}.reject! do |r| 
            args = @current_filter == 'all' ? [self, r] : [r]
            !filter.last.call(*args)
          end
        end
      end_src
    end

    def get_per_page_protected
      <<-end_src
        params[:no_paginate] ? 9999 : (#{@controller_class}.options[:per_page] || 10)
      end_src
    end
    
    # Crear paginador y limitar registros a mostrar según la página.
    def create_paginator_protected
      <<-end_src
        per_page = get_per_page
        page = params[:no_paginate] ? 1 : get_page
        offset = (page - 1) * per_page
        if @#{@modelos}
          count = @#{@modelos}.size
          @#{@modelos} = @#{@modelos}[offset..(offset + per_page - 1)]
          @#{@modelos}_paginator = Paginator.new(self, count, per_page, page)
        end
      end_src
    end
    
    def get_page_protected
      <<-end_src
        params[:page] ||= session[:#{@modelo}_page] || 1
        session[:#{@modelo}_page] = params[:page].to_i
      end_src
    end
    
    def parent_key
      @opciones[:foreign_key] || "#{@opciones[:parent]}_id"
    end
    
    def get_parent_protected
      if parent = @opciones[:parent]
        parent = parent.to_s
        code = <<-end_src
          @parent = @#{parent} = #{parent.classify}.find(params[:#{parent}_id])
        end_src
      else
        code = ''
      end
    end
    
    def get_current_protected
      <<-end_src
        begin
          @current = @#{@modelo} = #{@modelo_class}.find(params[:id])
        rescue ActiveRecord::RecordNotFound
          flash[:error] = "No existe el elemento solicitado."
          redirect_to #{@modelos}_url
          return false
        end
      end_src
    end
    
    # Devuelve un string con formato SQL para ordenar la lista.
    def get_order_protected
      <<-end_src
        if field = get_sort_by
          field + get_order_param
        else
          if session[:sort_by_#{@modelo}] && session[:order_#{@modelo}]
            session[:sort_by_#{@modelo}] + session[:order_#{@modelo}]
          else
            nil
          end
        end
      end_src
    end
    
    # Devuelve el campo por el cual se ordena.
    def get_sort_by_protected
      if @opciones[:orden].nil?
        code = sort_by_without_defa
      else
        code = <<-end_src
          field = sort_by_field || '#{@opciones[:orden]}'
        end_src
      end
      code += <<-end_src
        if #{@modelo_class}.content_columns.find{|c| c.name == field}
          session[:sort_by_#{@modelo}] = field
        else
          field = nil
        end
        field
      end_src
    end
    
    # Devuelve el campo por el cual se ordena cuando no hay uno asignado 
    # por defecto en el controller.
    def sort_by_without_defa
      <<-end_src
        if field = sort_by_field
          field
        else
          # Devuelve 'nombre' o el primer campo del modelo.
          #TODO: Debería ser el primer campo de la grilla.
          campos = #{@modelo_class}.columns.select {|c| c.name == 'nombre' }
          if !campos.blank?
            field = "nombre"
          else
            field = #{@modelo_class}.columns.first.name
          end
        end
      end_src
    end
    
    def sort_by_field_protected
      <<-end_src
        field = params[:sort_by]
        field = session[:sort_by_#{@modelo}] if !field && session[:sort_by_#{@modelo}]
        field
      end_src
    end
    
    def get_order_param_protected
      <<-end_src
        order = ''
        if params[:order]
          order = (params[:order] == "down") ? " DESC" :""
        else
          if session[:order_#{@modelo}]
            order = session[:order_#{@modelo}]
          end
        end
        session[:order_#{@modelo}] = order
        order
      end_src
    end
    
    def action!(action, protected = false)
      define! action, "#{action}_action", protected
    end
    
    def protected!(method, protected = false)
      define! method, "#{method}_protected", protected
    end
    
    def define!(method_name, callback, protected = false)
      "def #{method_name}\n#{send(callback)}\nend\n#{protected ? 'protected' : 'public'} :#{method_name}"
    end
    
    def my_url_for!
      <<-end_src
        def my_url_for_hash(controller_name, action, element = nil, parent = nil, params = {})
          url = params.merge :controller => controller_name, :action => action
          url.merge! :id => element if element
          if parent
            parent_sym = (parent.to_s + '_id').to_sym
            url.merge! parent_sym => instance_variable_get('@' + parent.to_s)
          end
          url
        end
        def my_url_for(controller_name, action, element = nil, parent = nil, params = {})
          url_for(my_url_for_hash(controller_name, action, element, parent, params))
        end
      end_src
    end
    
    def method_missing!
      <<-end_src
        @@optional_callbacks = [:before_assign, :before_save, :after_save, :before_render]
        alias_method :old_method_missing, :method_missing
        def method_missing(method, *args)
          return if @@optional_callbacks.include?(method)
          old_method_missing(method, args)
        end
      end_src
    end
    
  end
  
  module ClassMethods
    def layout_dynamic
      layout '../../vendor/plugins/navegador/files/app/views/layouts/base'
    end
    
    def tabs(*recursos)
      @right = recursos.last.is_a?(Array) ? recursos.pop : []
      @left  = [recursos].flatten
    end
    
    def left_tabs;  @left; end
    def right_tabs; @right; end
    
    def acts_as_dynamic
      define_method :left_tabs do 
        @tabs || ApplicationController.left_tabs || []
      end
      
      define_method :right_tabs do 
        rtn = @right_tabs || ApplicationController.right_tabs || []
        rtn
      end
    end
    
    def acts_as_dynamic_actions
      [:stylesheets, :images, :javascripts]
    end
    
    def generar(opciones = {})
      generator = Generator.new
      yield generator if block_given?
      
      @filters = generator.filters
      @actions = generator.actions
      @search_fields = generator.search_fields
      @buttons = generator.buttons
      @options = opciones
      add_code(generator, opciones)
      add_filters(generator, opciones)
      except = methods.include?('layout_except') ? layout_except : []
      layout "../../vendor/plugins/navegador/files/app/views/layouts/base", :except => except
    end
    
    def filters(name = nil)
      name && !name.to_s.empty? ? @filters.find {|f| f.first == name.to_sym} : @filters
    end
    
    def actions; @actions; end
    def search_fields; @search_fields; end
    def options; @options; end
    def buttons; @buttons; end
    def get_model; @model; end
    
    private
    
    def add_code(generator, opciones)
      code_generator = CodeGenerator.new(generator, set_model(opciones), opciones)
      actions = [:index, :new, :show, :create, :edit, :update, :destroy]
      decl = actions.inject([]) do |d, action| 
        d << code_generator.action!(action)
      end
      methods = [:render_index, :get_parent, :get_list, :get_list_find, :get_current, 
      :get_sort_by, :get_order, :sort_by_field, :get_order_param,
      :get_per_page, :apply_filter, :create_paginator, :get_page, :save_model, :get_inline_form_locals, :update_record_ajax]
      decl = methods.inject(decl) do |d, protected|
        d << code_generator.protected!(protected)
      end
      decl << code_generator.method_missing!
      decl << code_generator.my_url_for!
      class_eval decl.join("\n"), __FILE__, __LINE__
    end
    
    def add_filters(generator, opciones)
      except_filters = opciones[:except_filters].to_a || []
      before_filter :get_parent, 
        :except => except_filters + (opciones[:except_get_parent].to_a || [])
      before_filter :get_current, 
        :except => [:new, :create, :index] + except_filters + 
        generator.filter_methods + (opciones[:except_get_current].to_a || [])
    end
    
    def set_model(opciones)
      if opciones[:model]
        @model = opciones[:model]
      else
        class_name = self.to_s.underscore
        if class_name =~ /([\w]+)_controller$/
          @model = $1.singularize.to_sym
        else
          raise "generar() sólo puede usarse en controllers."
        end
        opciones[:model] = @model
      end
      @model
    end
    
  end
end

ActionController::Base.extend Navegador::ClassMethods
